//! Configurable JavaScript runtime for WebAssembly.
//!
//! Example usage:
//! ```
//! use anyhow::Result;
//! use javy::quickjs::{
//!     function::{MutFn, Rest},
//!     Ctx, Function, Value
//! };
//! use javy::{from_js_error, Runtime};
//!
//! fn main() -> Result<()> {
//!     let runtime = Runtime::default();
//!     let context = runtime.context();
//!
//!     context.with(|cx| {
//!         let globals = cx.globals();
//!         globals.set(
//!             "print_hello",
//!             Function::new(
//!                 cx.clone(),
//!                 MutFn::new(|_: Ctx<'_>, _: Rest<Value<'_>>| {
//!                     println!("Hello, world!");
//!                 }),
//!             )?,
//!         )
//!     })?;
//!
//!     context.with(|cx| {
//!         cx.eval_with_options("print_hello();", Default::default())
//!             .map_err(|e| from_js_error(cx.clone(), e))
//!             .map(|_: ()| ())
//!     })?;
//!
//!     Ok(())
//! }
//! ```
//!
//! ## Core concepts
//! * [`Runtime`] - The entrypoint for using the JavaScript runtime. Use a
//!   [`Config`] to configure behavior.
//!
//! ## Features
//! * `json` - functions for converting between [`quickjs::JSValueRef`] and JSON
//!   byte slices
//! * `messagepack` - functions for converting between [`quickjs::JSValueRef`]
//!   and MessagePack byte slices

pub use config::*;
pub use rquickjs as quickjs;
pub use runtime::Runtime;
use std::str;

mod config;
mod runtime;
mod serde;

use anyhow::{Error, Result, anyhow};
use rquickjs::{
    Ctx, Error as JSError, Exception, FromJs, String as JSString, Value, convert, prelude::Rest,
    qjs,
};

#[cfg(feature = "messagepack")]
pub mod messagepack;

#[cfg(feature = "json")]
pub mod json;

mod apis;

/// A struct to hold the current [`Ctx`] and [`Value`]s passed as arguments to Rust
/// functions.
/// A struct here is used to explicitly tie these values with a particular
/// lifetime.
//
// See: https://github.com/rust-lang/rfcs/pull/3216
pub struct Args<'js>(Ctx<'js>, Rest<Value<'js>>);

impl<'js> Args<'js> {
    /// Tie the [Ctx] and [Rest<Value>].
    pub fn hold(cx: Ctx<'js>, args: Rest<Value<'js>>) -> Self {
        Self(cx, args)
    }

    /// Get the [Ctx] and [Rest<Value>].
    pub fn release(self) -> (Ctx<'js>, Rest<Value<'js>>) {
        (self.0, self.1)
    }
}

/// Alias for [`Args::hold(cx, args).release()`]
#[macro_export]
macro_rules! hold_and_release {
    ($cx:expr, $args:expr) => {
        Args::hold($cx, $args).release()
    };
}

/// Alias for [`Args::hold`]
#[macro_export]
macro_rules! hold {
    ($cx:expr, $args:expr) => {
        Args::hold($cx, $args)
    };
}

/// Handles a JavaScript error or exception and converts to [anyhow::Error].
pub fn from_js_error(ctx: Ctx<'_>, e: JSError) -> Error {
    if e.is_exception() {
        let val = ctx.catch();

        if let Some(exception) = val.clone().into_exception() {
            anyhow!("{exception}")
        } else {
            anyhow!(val_to_string(&ctx, val).unwrap_or_else(|_| "Internal error".to_string()))
        }
    } else {
        Into::into(e)
    }
}

/// Converts an [`anyhow::Error`]  to a [`JSError`].
///
/// If the error is an [`anyhow::Error`] this function will construct and throw
/// a JS [`Exception`] in order to construct the [`JSError`].
pub fn to_js_error(cx: Ctx, e: Error) -> JSError {
    match e.downcast::<JSError>() {
        Ok(e) => e,
        Err(e) => {
            // In some cases the original error context is lost i.e. we can't
            // retain the orginal JSError when invoking serde_transcode,
            // particularly for json::stringify. The process of transcoding will
            // report the Serializer error, which is totally implementation
            // dependent, in this case particular to serde_json::Error. To
            // workaround this, we identify the exception via its string
            // representation. This is not ideal, but its also fine as it only
            // happens in the transcoding case.
            //
            // Ref: https://github.com/sfackler/serde-transcode/issues/8
            if e.to_string()
                .contains("JSError: Exception generated by QuickJS")
            {
                return JSError::Exception;
            }

            cx.throw(Value::from_exception(
                Exception::from_message(cx.clone(), &e.to_string())
                    .expect("creating an exception to succeed"),
            ))
        }
    }
}

/// Converts the JavaScript value to a string, replacing any invalid UTF-8 sequences with the
/// Unicode replacement character (U+FFFD).
// TODO: Upstream this?
pub fn to_string_lossy<'js>(cx: &Ctx<'js>, string: &JSString<'js>, error: JSError) -> String {
    let mut len: qjs::size_t = 0;
    let ptr =
        unsafe { qjs::JS_ToCStringLen2(cx.as_raw().as_ptr(), &mut len, string.as_raw(), false) };
    let buffer = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) };

    // The error here *must* be a Utf8 error; the `JSString::to_string()` may
    // return `JSError::Unknown`, but at that point, something else has gone
    // wrong too.

    let mut utf8_error = match error {
        JSError::Utf8(e) => e,
        _ => unreachable!("expected Utf8 error"),
    };
    let mut res = String::new();
    let mut buffer = buffer;
    loop {
        let (valid, after_valid) = buffer.split_at(utf8_error.valid_up_to());
        res.push_str(unsafe { str::from_utf8_unchecked(valid) });
        res.push(char::REPLACEMENT_CHARACTER);

        // see https://simonsapin.github.io/wtf-8/#surrogate-byte-sequence
        let lone_surrogate = matches!(after_valid, [0xED, 0xA0..=0xBF, 0x80..=0xBF, ..]);

        // https://simonsapin.github.io/wtf-8/#converting-wtf-8-utf-8 states that each
        // 3-byte lone surrogate sequence should be replaced by 1 UTF-8 replacement
        // char. Rust's `Utf8Error` reports each byte in the lone surrogate byte
        // sequence as a separate error with an `error_len` of 1. Since we insert a
        // replacement char for each error, this results in 3 replacement chars being
        // inserted. So we use an `error_len` of 3 instead of 1 to treat the entire
        // 3-byte sequence as 1 error instead of as 3 errors and only emit 1
        // replacement char.
        let error_len = if lone_surrogate {
            3
        } else {
            utf8_error
                .error_len()
                .expect("Error length should always be available on underlying buffer")
        };

        buffer = &after_valid[error_len..];
        match str::from_utf8(buffer) {
            Ok(valid) => {
                res.push_str(valid);
                break;
            }
            Err(e) => utf8_error = e,
        }
    }
    res
}

/// Retrieves the string representation of a JavaScript value.
pub fn val_to_string<'js>(this: &Ctx<'js>, val: Value<'js>) -> Result<String> {
    if let Some(symbol) = val.as_symbol() {
        if let Some(description) = symbol.description()?.into_string() {
            let description = description
                .to_string()
                .unwrap_or_else(|e| to_string_lossy(this, &description, e));
            Ok(format!("Symbol({description})"))
        } else {
            Ok("Symbol()".into())
        }
    } else {
        let stringified = <convert::Coerced<JSString>>::from_js(this, val).map(|string| {
            string
                .to_string()
                .unwrap_or_else(|e| to_string_lossy(this, &string.0, e))
        })?;
        Ok(stringified)
    }
}
